import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;

public class ServerTCP extends Thread
{
    private String directory;
    private long capacity;
    private long used;
    private HashMap<String,ClientThread> clients;
    private HashMap<String,RepFile> files; 
    private ArrayList<String> transferringFiles;
    private HashMap<String,ArrayList<String>> readCopies; //user and filenames
    private HashMap<String,ArrayList<String>> editCopies;
    private ServerSocket ss = null;
    
    public ServerTCP(String dir, long cap)
    {
        clients = new HashMap<>();
        files = new HashMap<>();
        transferringFiles = new ArrayList<>();
        editCopies = new HashMap<>();
        readCopies = new HashMap<>();
        directory = dir;
        capacity = cap;
        used = 0;
        loadFiles();
    }
    
    public synchronized String getDirectory()
    {
        return directory;
    }
    private synchronized void loadFiles()
    {
        ObjectInputStream in;
        
        try
        {
            File file = new File(directory + Constants.FILES_FILE);
            
            if (file.exists())
            {
                in = new ObjectInputStream(new FileInputStream(file));
                
                try
                {
                    while(true)
                    {
                        RepFile rf = (RepFile)in.readObject();
                        files.put(rf.getName(),rf);
                        used += rf.getLength();
                    }
                }
                catch(EOFException e){}
                finally
                {
                    in.close();
                }
            }
        }
        catch(Exception e){System.out.println("Exception loading files file: " + e);}
    }
    private synchronized void saveFiles()
    {
        ObjectOutputStream out;
        
        File file = new File(directory + Constants.FILES_FILE);
        try 
        {
            out = new ObjectOutputStream(new FileOutputStream(file));
            
            for(RepFile rf: files.values())
            {
                out.writeObject(rf);
                out.flush();
            }
            out.close();
        } 
        catch (IOException ex){System.out.println("Exception saving into files file: " + ex);}
    }
    public synchronized void addTransferringFile(String fileName)
    {
        transferringFiles.add(fileName);
    }
    public synchronized void addFile(String fileName, String author)
    {        
        File f = new File(directory + fileName);
        files.put(fileName,new RepFile(fileName, author, f.length()));
        transferringFiles.remove(fileName);
        used += f.length();
        saveFiles();
    }
    public synchronized void updateFile(String fileName)
    {
        File f = new File(directory + fileName);

        used += f.length() - files.get(fileName).getLength();
        files.get(fileName).setLastModified(f.lastModified());
        files.get(fileName).setSize(f.length());
        fileUpdated(fileName);
        saveFiles();
    }
    public synchronized Message deleteFile(String fileName)
    {
        String author = files.get(fileName).getAuthor();
        if (new File(directory + fileName).delete())
        {
            fileDeleted(author,fileName);
            used -= files.remove(fileName).getLength();
            saveFiles();
            return new Message(Constants.INFO, new String[]{Constants.DELETED + fileName});
        }
        return new Message(Constants.INFO, new String[]{Constants.WARNING + fileName + Constants.IS_BEING_USED});
    }
    private synchronized void fileDeleted(String author, String fileName)
    {
        for (String user: readCopies.keySet())
        {
            if (readCopies.get(user).contains(fileName))
            {
                discardReadCopy(user, fileName);
                clients.get(user).fileDeleted(fileName);
            }
        }
        if (editCopies.containsKey(author) && editCopies.get(author).contains(fileName))
        {
            discardEditCopy(author, fileName);
            clients.get(author).fileDeleted(fileName);
        }
    }
    private synchronized void fileUpdated(String fileName)
    {
        for (String user: readCopies.keySet())
        {
            if (readCopies.get(user).contains(fileName))
            {
                clients.get(user).fileUpdated(fileName);
            }
        }
    }
    public synchronized boolean fileExists(String fileName)
    {
        return files.containsKey(fileName) || transferringFiles.contains(fileName);
    }
    public synchronized boolean spaceExists(String fileName, long fileSize)
    {
        if (fileName != null)
        {
            if ((used + fileSize - files.get(fileName).getLength()) < capacity)
            {
                return true;
            }
            return false;
        }
        if (used + fileSize < capacity)
        {
            return true;
        }
        return false;
    }
    public synchronized RepFile getRepfile(String fileName)
    {        
        return files.get(fileName);
    }    
    private synchronized boolean addClient(ClientThread t, String user)
    {
        if (clients.containsKey(user))
        {
            return false;
        }
        clients.put(user,t);
        System.out.println("Cliente aceite: " + t.getSocket().getInetAddress());
        return true;
    }
    private synchronized void removeClient(String user)
    {
        System.out.println("Cliente com " + clients.get(user).getSocket().getInetAddress() + " desligou-se!");
        clients.remove(user);
        editCopies.remove(user);
        readCopies.remove(user);
    }
    public synchronized Message registerClient(ClientThread t, String username, String password)
    {
        ObjectInputStream in;
        ObjectOutputStream out;
        boolean found = false;
        Account acc;
        
        try
        {
            File file = new File(directory + Constants.ACCOUNTS_FILE);
            
            if (file.exists())
            {
                in = new ObjectInputStream(new FileInputStream(file));
                
                try
                {
                    while(true)
                    {
                        acc = (Account)in.readObject();
                        if (acc.getUser().equals(username))
                        {
                            found = true;
                            break;
                        }
                    }
                }
                catch(EOFException e){}
                in.close();
                if(found) //username já existe
                {
                    return new Message(Constants.INSUCCESS, new String[] {Constants.ALREADY_EXISTS_ACCOUNT + username});
                }
                out = new AppendingObjectOutputStream(new FileOutputStream(file,true));
            }
            else
            {
                out = new ObjectOutputStream(new FileOutputStream(file));
            }
            
            out.writeObject(new Account(username,password));
            out.flush();
            out.close();
            
            t.setUsername(username);
            addClient(t, username);
            return new Message(Constants.SUCCESS, null);
        }
        catch(Exception e){System.out.println("Exception using accounts file: " + e);}
        return null;
    }
    public synchronized Message loginClient(ClientThread t, String username, String password)
    {
        ObjectInputStream in;
        boolean found = false;
        Account acc = null;
        
        try
        {
            File file = new File(directory + Constants.ACCOUNTS_FILE);
            
            if (file.exists())
            {
                in = new ObjectInputStream(new FileInputStream(file));
                try
                {
                    while(true)
                    {
                        acc = (Account)in.readObject();
                        if (acc.getUser().equals(username))
                        {
                            found = true;
                            break;
                        }
                    }
                }
                catch(EOFException e){}
                in.close();
                if(found) //username existe
                {
                    if (acc.getPass().equals(password))
                    {
                        if (addClient(t, username))
                        {
                            t.setUsername(username);
                            return new Message(Constants.SUCCESS, null);
                        }
                        return new Message(Constants.INSUCCESS, new String[] {Constants.ALREADY_LOGGED});
                    }
                    return new Message(Constants.INSUCCESS, new String[] {Constants.INCORRECT_PASSWORD});
                }
            }                       
            return new Message(Constants.INSUCCESS, new String[] {Constants.NONEXISTENT_ACCOUNT});
        }
        catch(Exception e){System.out.println("Exception using files: " + e);}
        return null;
    }
    public synchronized void logout(String user)
    {
        clients.get(user).setUsername(null);
        removeClient(user);
        System.out.println("Removido as " + new Date(System.currentTimeMillis()));
    }
    public synchronized void editCopydownloaded(String user, String fileName)
    {        
        if (editCopies.containsKey(user))
        {
            editCopies.get(user).add(fileName);
            return;
        }
        ArrayList<String> array = new ArrayList<>();
        array.add(fileName);
        editCopies.put(user,array);
    }
    public synchronized void readCopydownloaded(String user, String fileName)
    {        
        if (readCopies.containsKey(user))
        {
            readCopies.get(user).add(fileName);
            return;
        }
        ArrayList<String> array = new ArrayList<>();
        array.add(fileName);
        readCopies.put(user,array);
    }
    public synchronized void discardEditCopy(String user, String fileName)
    {
        if (editCopies.get(user).size() == 1)
        {
            editCopies.remove(user);
            return;
        }
        editCopies.get(user).remove(fileName);
    }
    public synchronized void discardReadCopy(String user, String fileName)
    {
        if (readCopies.get(user).size() == 1)
        {
            readCopies.remove(user);
            return;
        }
        readCopies.get(user).remove(fileName);      
    }   
    public synchronized void update()
    {
        Message m = new Message(Constants.NOTIFY_CLIENT, new Object[] {new ArrayList(files.values()), new ArrayList(clients.keySet()), editCopies, readCopies});
        
        for(ClientThread t: clients.values())
        {
            t.update(m);
        }
    }
    
    @Override
    public void run()
    {
        Socket s;
        
        try
        {
            ss = new ServerSocket(Constants.TCP_PORT);
            while(true)
            {
                System.out.println("Estao " + clients.size() + " clientes ligados");
                System.out.println("Server TCP waiting for connections...");
                s = ss.accept();
                new ClientThread(this,s).start();
            }
        }
        catch(Exception e){System.out.println("Excepção: " + e);}
    }
}
